﻿/******************************************************************************************************
Copyright (c) 2014 Steven Giacomelli (stevegiacomelli@gmail.com)

Permission is hereby granted, free of charge, to any person obtaining a copy of this 
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify, 
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 
permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies 
or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************************************************************************************/

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace SharpGeo
{
    /// <summary>
    /// An immutable envelope (axis aligned bounding box)
    /// </summary>
    [DebuggerDisplay("({MinX},{MinY},{MaxX},{MaxY})")]
    [StructLayout(LayoutKind.Sequential)]
    public struct Envelope :
        IEquatable<Envelope>,
        IComparable<Envelope>
    {
        //****************************** Key assumptions *****************************************
        //* 1 - The Envelope will be used to quickly test if geometry intersects, overlaps etc   *
        //* 2 - The stored values will represent Lat/Long                                        *
        //* 3 - These comparisons will be done at at a scale greater than 11cms                  *
        //* As a result of these assumptions no more than 6 decimals of precision are required   *
        //* The values are thus transparently stored as ints                                     *
        //****************************************************************************************

        #region Constructors
        /// <summary>
        /// Private constructor allows for the creation of an infinite Envelope
        /// </summary>
        /// <param name="x1">The first X value</param>
        /// <param name="y1">The first Y value</param>
        /// <param name="x2">The second X value</param>
        /// <param name="y2">The second Y value</param>
        /// <param name="unsorted">True if list is not to be sorted with Min/Max</param>
        private Envelope(double x1, double y1, double x2, double y2, bool unsorted)
        {
            if (unsorted)
            {
                _minX = (int)(x1 * ConversionFactor);
                _minY = (int)(y1 * ConversionFactor);
                _maxX = (int)(x2 * ConversionFactor);
                _maxY = (int)(y2 * ConversionFactor);
            }
            else
            {
                _minX = (int)((x1 < x2 ? x1 : x2) * ConversionFactor);
                _minY = (int)((y1 < y2 ? y1 : y2) * ConversionFactor);
                _maxX = (int)((x1 > x2 ? x1 : x2) * ConversionFactor);
                _maxY = (int)((y1 > y2 ? y1 : y2) * ConversionFactor);
            }
        }

        /// <summary>
        /// Constructs a new Envelope
        /// </summary>
        /// <param name="x1">The first X value</param>
        /// <param name="y1">The first Y value</param>
        /// <param name="x2">The second X value</param>
        /// <param name="y2">The second Y value</param>
        public Envelope(double x1, double y1, double x2, double y2)
            : this(x1, y1, x2, y2, false)
        {
        }

        /// <summary>
        /// Private constructor allows for the creation of the infinite Envelope
        /// </summary>
        /// <param name="x1">The first X value</param>
        /// <param name="y1">The first Y value</param>
        /// <param name="x2">The second X value</param>
        /// <param name="y2">The second Y value</param>
        /// <param name="unsorted">True if list is not to be sorted with Min/Max</param>
        private Envelope(int x1, int y1, int x2, int y2, bool unsorted)
        {
            if (unsorted)
            {
                _minX = x1;
                _minY = y1;
                _maxX = x2;
                _maxY = y2;
            }
            else
            {
                _minX = x1 < x2 ? x1 : x2;
                _minY = y1 < y2 ? y1 : y2;
                _maxX = x1 > x2 ? x1 : x2;
                _maxY = y1 > y2 ? y1 : y2;
            }
        }
        #endregion

        #region Fields
        private const double ConversionFactor = 1000000D;
        private int _minX;
        private int _minY;
        private int _maxX;
        private int _maxY;

        /// <summary>
        /// Returns an empty Envelope (infinite)
        /// </summary>
        public static readonly Envelope Empty = new Envelope(
                    int.MaxValue, int.MaxValue,
                    int.MinValue, int.MinValue, true);
        #endregion

        #region Properties
        /// <summary>
        /// Gets the minimum X value
        /// </summary>
        public double MinX
        {
            get
            {
                return ((double)_minX) / ConversionFactor;
            }
        }

        /// <summary>
        /// Gets the minimum y value
        /// </summary>
        public double MinY
        {
            get
            {
                return ((double)_minY) / ConversionFactor;
            }
        }

        /// <summary>
        /// Gets the maximum X value
        /// </summary>
        public double MaxX
        {
            get
            {
                return ((double)_maxX) / ConversionFactor;
            }
        }

        /// <summary>
        /// Gets the maximum Y value
        /// </summary>
        public double MaxY
        {
            get
            {
                return ((double)_maxY) / ConversionFactor;
            }
        }

        /// <summary>
        /// Gets the width of this Envelope
        /// </summary>
        public double Width { get { return MaxX - MinX; } }

        /// <summary>
        /// Gets the height of this Envelope
        /// </summary>
        public double Height { get { return MaxY - MinY; } }

        /// <summary>
        /// Gets the area of this Envelope (note: this value is only useful for comparison, it is meaningless outside of this context)
        /// </summary>
        public int Area
        {
           get{ return (_maxX - _minX) * (_maxY - _minY);
           }
        }
        /// <summary>
        /// Gets the center of this Envelope
        /// </summary>
        public ICoordinate CenterPoint
        {
            get
            {
                return new GeographicCoordinate
                    ((MinX + MaxX) / 2D,
                    (MinY + MaxY) / 2D);
            }
        }
        #endregion

        #region Overloaded Operators
        /// <summary>
        /// Compares two Envelopes for equality
        /// </summary>
        /// <param name="envelope1">The first Envelope to compare</param>
        /// <param name="envelope2">The second Envelope to compare</param>
        /// <returns>Returns true if both Envelopes are equal</returns>
        public static bool operator ==(Envelope envelope1, Envelope envelope2)
        {
            return
                envelope1._minX == envelope2._minX &&
                envelope1._minY == envelope2._minY &&
                envelope1._maxX == envelope2._maxX &&
                envelope1._maxY == envelope2._maxY;
        }

        /// <summary>
        /// Compares two Envelopes for inequality
        /// </summary>
        /// <param name="envelope1">The first Envelope to compare</param>
        /// <param name="envelope2">The second Envelope to compare</param>
        /// <returns>Returns true if both Envelopes are not equal</returns>
        public static bool operator !=(Envelope envelope1, Envelope envelope2)
        {
            return !(envelope1 == envelope2);
        }

        /// <summary>
        /// Compares two Envelopes for equality
        /// </summary>
        /// <param name="obj">The envelope to compare</param>
        /// <returns>Returns true if both Envelopes are equal</returns>
        public override bool Equals(object obj)
        {
            if (obj == null || !(obj is Envelope))
                return false;

            return (this == (Envelope)obj);
        }

        /// <summary>
        /// Compares two Envelopes for equality
        /// </summary>
        /// <param name="other">The envelope to compare</param>
        /// <returns>Returns true if both Envelopes are equal</returns>
        bool IEquatable<Envelope>.Equals(Envelope other)
        {
            return this == other;
        }

        /// <summary>
        /// Returns a Envelope containing the combined area of the operands
        /// </summary>
        /// <param name="envelope1">The first Envelope to combine</param>
        /// <param name="envelope2">The second Envelope to combine</param>
        /// <returns>A Envelope combining the operands</returns>
        public static Envelope operator +(Envelope envelope1, Envelope envelope2)
        {
            return new Envelope(
                envelope1._minX < envelope2._minX ? envelope1._minX : envelope2._minX,
                envelope1._minY < envelope2._minY ? envelope1._minY : envelope2._minY,
                envelope1._maxX > envelope2._maxX ? envelope1._maxX : envelope2._maxX,
                envelope1._maxY > envelope2._maxY ? envelope1._maxY : envelope2._maxY,
                true);
        }

        /// <summary>
        /// Returns a Envelope containing the combined area of the operands
        /// </summary>
        /// <param name="envelope1">The first Envelope to combine</param>
        /// <param name="envelope2">The second Envelope to combine</param>
        /// <returns>A Envelope combining the operands</returns>
        public static Envelope Add(Envelope envelope1, Envelope envelope2)
        {
            return envelope1 + envelope2;
        }

        /// <summary>
        /// Compares the area of two Envelopes using the greater than operator
        /// </summary>
        /// <param name="envelope1">The first Envelope to compare</param>
        /// <param name="envelope2">The second Envelope to compare</param>
        /// <returns>Returns true if the area of env1 is greater than the area of env2</returns>
        public static bool operator >(Envelope envelope1, Envelope envelope2)
        {
            return envelope1.Area > envelope2.Area;
        }

        /// <summary>
        /// Compares the area of two Envelopes using the greater than or equal to operator
        /// </summary>
        /// <param name="envelope1">The first Envelope to compare</param>
        /// <param name="envelope2">The second Envelope to compare</param>
        /// <returns>Returns true if the area of env1 is greater than or equal to the area of env2</returns>
        public static bool operator >=(Envelope envelope1, Envelope envelope2)
        {
            return envelope1.Area >= envelope2.Area;
        }

        /// <summary>
        /// Compares the area of two Envelopes using the less than operator
        /// </summary>
        /// <param name="envelope1">The first Envelope to compare</param>
        /// <param name="envelope2">The second Envelope to compare</param>
        /// <returns>Returns true if the area of env1 is less than the area of env2</returns>
        public static bool operator <(Envelope envelope1, Envelope envelope2)
        {
            return envelope1.Area < envelope2.Area;
        }

        /// <summary>
        /// Compares the area of two Envelopes using the less than or equal to operator
        /// </summary>
        /// <param name="envelope1">The first Envelope to compare</param>
        /// <param name="envelope2">The second Envelope to compare</param>
        /// <returns>Returns true if the area of env1 is less than or equal to the area of env2</returns>
        public static bool operator <=(Envelope envelope1, Envelope envelope2)
        {
            return envelope1.Area <= envelope2.Area;
        }

        /// <summary>
        /// Compares the area of two Envelopes
        /// </summary>
        /// <param name="other">The other Envelope to compare</param>
        /// <returns>Returns zero if the area of both Envelopes is equal, -1 if the area of the other is greater, and +1 if this area is greater</returns>
        int IComparable<Envelope>.CompareTo(Envelope other)
        {
            return Compare(other);
        }

        /// <summary>
        /// Compares the area of two Envelopes
        /// </summary>
        /// <param name="other">The other Envelope to compare</param>
        /// <returns>Returns zero if the area of both Envelopes is equal, -1 if the area of the other is greater, and +1 if this area is greater</returns>
        public int Compare(Envelope other)
        {
            var a1 = Area;
            var a2 = other.Area;

            if (a1 < a2)
            {
                return -1;
            }

            if (a1 > a2)
            {
                return 1;
            }

            return 0; 
        }
        #endregion

        #region Methods
        /// <summary>
        /// Determines if this Envelope contains the passed Envelope.
        /// </summary>
        /// <param name="other">The passed Envelope</param>
        /// <returns>If this Envelope contains the passed Envelope </returns>
        public bool Contains(Envelope other)
        {
            return !(
                _minX > other._minX || _maxX < other._maxX ||
                _minY > other._minY || _maxY < other._maxY);
        }

        /// <summary>
        /// Determines if this Envelope contains the passed ICoordinate
        /// </summary>
        /// <param name="coordinate">The passed ICoordinate</param>
        /// <returns>Returns true if this Envelope contains the passed ICoordinate</returns>
        public bool Contains(ICoordinate coordinate)
        {
            if (coordinate == null)
                throw new ArgumentNullException("coordinate",
                    Properties.Resources.EnvelopeCoordinateNullException);

            return !(MinX > coordinate[0] || MaxX < coordinate[0] || MinY > coordinate[1] || MaxY < coordinate[1]);
        }

        /// <summary>
        /// Determines if this Envelope intersects the passed Envelope
        /// </summary>
        /// <param name="other">The passed Envelope</param>
        /// <returns>True if this Envelope intersects the passed Envelope</returns>
        public bool Intersects(Envelope other)
        {
            return !(
                _minX > other._maxX || _maxX < other._minX ||
                _minY > other._maxY || _maxY < other._minY);
        }

        /// <summary>
        /// Returns the enlarged area resulting from the combination of this Envelope and the passed Envelope (note: this value is only useful for comparison, it is meaningless outside of this context)
        /// </summary>
        /// <param name="other">The passed Envelope</param>
        /// <returns>The enlarged area from the combination of the two Envelopes (note: this value is only useful for comparison, it is meaningless outside of this context)</returns>
        public int GetEnlargement(Envelope other)
        {
            int enlargedArea =
                ((_maxX > other._maxX ? _maxX : other._maxX) - (_minX < other._minX ? _minX : other._minX)) *
                ((_maxY > other._maxY ? _maxY : other._maxY) - (_minY < other._minY ? _minY : other._minY));

            return enlargedArea - Area;
        }        

        /// <summary>
        /// Returns the hashcode of this Envelope
        /// </summary>
        /// <returns>The computed hashcode of this Envelope</returns>
        public override int GetHashCode()
        {
            long l = unchecked(
                _minX +
                (37 * _minY) +
                (43 * _maxX) +
                (47 * _maxY));

            return (int)unchecked((l >> 32) ^ l);
        }

        /// <summary>
        /// Converts this Envelope to a 16 byte array
        /// </summary>
        /// <returns>A 16 byte array representing this envelope</returns>
        public byte[] ToByteArray()
        {
            return new byte[16]{
                (byte)(_minX >> 24),
                (byte)(_minX >> 16),
                (byte)(_minX >> 8),
                (byte)_minX,

                (byte)(_minY >> 24),
                (byte)(_minY >> 16),
                (byte)(_minY >> 8),
                (byte)_minY,

                (byte)(_maxX >> 24),
                (byte)(_maxX >> 16),
                (byte)(_maxX >> 8),
                (byte)_maxX,

                (byte)(_maxY >> 24),
                (byte)(_maxY >> 16),
                (byte)(_maxY >> 8),
                (byte)_maxY};
        }

        /// <summary>
        /// Returns a string representing this envelope
        /// </summary>
        /// <returns>A string representing this envelope</returns>
        public override string ToString()
        {
            return string.Format(System.Globalization.CultureInfo.InvariantCulture,
                "({0},{1},{2},{3})", MinX, MaxX, MinY, MaxY);
        }

        #endregion
    }
}